今天我們要實作一個檔案搜尋工具,類似於 Unix/Linux 系統中的 grep 命令。這個專案將幫助我們學習 Rust 中的檔案處理、字串匹配、命令列參數解析,以及錯誤處理等重要概念。
首先我們先建立 cargo 專案,至於前面做過我們就不贅述
[package]
name = "test-grep"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.47", features = ["derive"] }
colored = "3.0.0"
regex = "1.11.2"
walkdir = "2.5.0"
我們會用到 clap
colored
regex
walkdir
clap
定義命令列參數 :use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
/// 要搜尋的模式
#[arg(value_name = "PATTERN")]
pub pattern: String,
/// 要搜尋的檔案路徑
#[arg(value_name = "FILE")]
pub file: String,
/// 忽略大小寫
#[arg(short, long)]
pub ignore_case: bool,
/// 顯示行號
#[arg(short, long)]
pub line_number: bool,
/// 使用正則表達式
#[arg(short, long)]
pub regex: bool,
/// 遞迴搜尋目錄
#[arg(short = 'R', long)]
pub recursive: bool,
/// 只顯示檔案名稱
#[arg(short, long)]
pub files_with_matches: bool,
}
use regex::Regex;
use std::error::Error;
pub struct SearchEngine {
pattern: String,
ignore_case: bool,
use_regex: bool,
compiled_regex: Option<Regex>,
}
impl SearchEngine {
pub fn new(pattern: String, ignore_case: bool, use_regex: bool) -> Result<Self, Box<dyn Error>> {
let compiled_regex = if use_regex {
let regex_pattern = if ignore_case {
format!("(?i){}", pattern)
} else {
pattern.clone()
};
Some(Regex::new(®ex_pattern)?)
} else {
None
};
Ok(SearchEngine {
pattern,
ignore_case,
use_regex,
compiled_regex,
})
}
pub fn matches(&self, line: &str) -> bool {
if let Some(ref regex) = self.compiled_regex {
regex.is_match(line)
} else if self.ignore_case {
line.to_lowercase().contains(&self.pattern.to_lowercase())
} else {
line.contains(&self.pattern)
}
}
}
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use colored::*;
pub struct SearchResult {
pub file_path: String,
pub line_number: usize,
pub line_content: String,
}
pub fn search_file(
file_path: &Path,
engine: &SearchEngine,
show_line_numbers: bool,
) -> Result<Vec<SearchResult>, Box<dyn Error>> {
let file = File::open(file_path)?;
let reader = BufReader::new(file);
let mut results = Vec::new();
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
if engine.matches(&line) {
results.push(SearchResult {
file_path: file_path.to_string_lossy().to_string(),
line_number: line_num + 1,
line_content: line,
});
}
}
Ok(results)
}
pub fn display_results(
results: &[SearchResult],
pattern: &str,
show_line_numbers: bool,
files_with_matches: bool,
) {
if files_with_matches {
let mut displayed_files = std::collections::HashSet::new();
for result in results {
if displayed_files.insert(&result.file_path) {
println!("{}", result.file_path.green());
}
}
return;
}
for result in results {
let highlighted_line = highlight_matches(&result.line_content, pattern);
if show_line_numbers {
println!(
"{}:{}:{}",
result.file_path.green(),
result.line_number.to_string().yellow(),
highlighted_line
);
} else {
println!("{}:{}", result.file_path.green(), highlighted_line);
}
}
}
fn highlight_matches(line: &str, pattern: &str) -> String {
line.replace(pattern, &pattern.red().bold().to_string())
}
這時候考慮 recursive 搜尋
使用 walkdir
實現
use walkdir::WalkDir;
pub fn search_directory(
dir_path: &Path,
engine: &SearchEngine,
show_line_numbers: bool,
) -> Result<Vec<SearchResult>, Box<dyn Error>> {
let mut all_results = Vec::new();
for entry in WalkDir::new(dir_path) {
let entry = entry?;
let path = entry.path();
if path.is_file() {
// 過濾掉二進位檔案(簡單檢查)
if let Some(extension) = path.extension() {
let ext_str = extension.to_string_lossy().to_lowercase();
if matches!(ext_str.as_str(), "exe" | "bin" | "obj" | "dll" | "so") {
continue;
}
}
match search_file(path, engine, show_line_numbers) {
Ok(mut results) => all_results.append(&mut results),
Err(e) => eprintln!("警告:無法搜尋檔案 {}: {}", path.display(), e),
}
}
}
Ok(all_results)
}
use clap::Parser;
use std::path::Path;
use std::process;
mod search;
use search::*;
fn main() {
let args = Args::parse();
// 建立搜尋引擎
let engine = match SearchEngine::new(
args.pattern.clone(),
args.ignore_case,
args.regex,
) {
Ok(engine) => engine,
Err(e) => {
eprintln!("錯誤:無法建立搜尋引擎: {}", e);
process::exit(1);
}
};
let path = Path::new(&args.file);
let results = if args.recursive {
if !path.is_dir() {
eprintln!("錯誤:遞迴模式需要提供目錄路徑");
process::exit(1);
}
search_directory(path, &engine, args.line_number)
} else {
if !path.is_file() {
eprintln!("錯誤:指定的路徑不是有效的檔案");
process::exit(1);
}
search_file(path, &engine, args.line_number)
};
match results {
Ok(results) => {
if results.is_empty() {
println!("沒有找到匹配的結果");
process::exit(1);
} else {
display_results(&results, &args.pattern, args.line_number, args.files_with_matches);
println!("\n找到 {} 個匹配結果", results.len());
}
}
Err(e) => {
eprintln!("搜尋過程中發生錯誤: {}", e);
process::exit(1);
}
}
}
cargo build --release
# 基本搜尋
./target/release/rusty-grep "function" src/main.rs
# 忽略大小寫搜尋
./target/release/rusty-grep -i "FUNCTION" src/main.rs
# 顯示行號
./target/release/rusty-grep -n "function" src/main.rs
# 使用正則表達式
./target/release/rusty-grep -r "fn \w+\(" src/
# 遞迴搜尋目錄
./target/release/rusty-grep -R "TODO" ./src/
# 只顯示包含匹配的檔案名稱
./target/release/rusty-grep -l "error" ./src/
今天我們成功實作了一個功能豐富的檔案搜尋工具,涵蓋了以下 Rust 重要概念:
命令列參數解析:使用 clap crate 建立使用者友善的 CLI
檔案 I/O:安全高效地讀取檔案內容
正則表達式:使用 regex crate 進行模式匹配
錯誤處理:適當的錯誤傳播和處理
模組化設計:將功能分解為可重用的組件